package coop.bancocredicoop.proyectos.gd.alfresco;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.ContentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.gson.Gson;

import coop.bancocredicoop.proyectos.gd.domain.Document;

public abstract class AlfrescoHttpClient {

    private static final Log log = LogFactory.getLog(AlfrescoHttpClient.class);

    private AlfrescoConfig alfrescoConfig;
    private String serviceURL;
    private String baseURL;

    AlfrescoHttpClient(AlfrescoConfig alfrescoConfig) {
    	this.alfrescoConfig = alfrescoConfig;
        baseURL = alfrescoConfig.getBaseURL();
        serviceURL = alfrescoConfig.getServiceURL();
    }

    public URIBuilder getUrl(String relativeURL) throws URISyntaxException {
        String url = String.format("%s%s", baseURL, relativeURL);
        return new URIBuilder(url);        
    }

    abstract public void beforeGet(HttpClient client, HttpGet get);
    
    abstract public void beforePost(HttpClient client, HttpPost post);

    private void addParametersToURLBuilder(URIBuilder builder, HashMap<String, String> parameters) {
        for (String parameterKey : parameters.keySet()) {
        	builder.addParameter(parameterKey, parameters.get(parameterKey));
        }
    }
    
    public HttpResponse get(HttpClient httpClient, String relativeURL, HashMap<String, String> parameters) throws URISyntaxException, ClientProtocolException, IOException {
        
        URIBuilder alfrescoURLBuilder = getUrl(relativeURL);
        addParametersToURLBuilder(alfrescoURLBuilder, parameters);

        HttpGet httpGet = new HttpGet(alfrescoURLBuilder.build());
        beforeGet(httpClient, httpGet);
        return httpClient.execute(httpGet);
    
    }

    public HttpResponse post(HttpClient httpClient, String relativeURL, HashMap<String, String> parameters) throws URISyntaxException, ClientProtocolException, IOException {        

        URIBuilder alfrescoURLBuilder = getUrl(relativeURL);
        HttpPost httpPost = new HttpPost(alfrescoURLBuilder.build());        
        List<NameValuePair> postParameters = new ArrayList<NameValuePair>();
        
        for (String parameterKey : parameters.keySet()) {
        	postParameters.add(new BasicNameValuePair(parameterKey, parameters.get(parameterKey)));
        }

        httpPost.setEntity(new UrlEncodedFormEntity(postParameters));
        beforePost(httpClient, httpPost);
        return httpClient.execute(httpPost);
        
    }

    private HttpResponse postMultipart(HttpClient httpClient, String relativeURL, String fileName, InputStream inputStream, HashMap<String, String> stringParameters) throws Exception {

        URIBuilder alfrescoURLBuilder = getUrl(relativeURL);
        HttpPost httpPost = new HttpPost(alfrescoURLBuilder.build());
        MultipartEntityBuilder reqEntityBuilder = MultipartEntityBuilder.create();;
        reqEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);

        reqEntityBuilder.addBinaryBody("file", 
        							   inputStream,
                                       ContentType.DEFAULT_BINARY, 
                                       fileName);
        
        for (String parameterKey : stringParameters.keySet()) {
        	reqEntityBuilder.addTextBody(parameterKey, stringParameters.get(parameterKey));
        }

        httpPost.setEntity(reqEntityBuilder.build());
        beforePost(httpClient, httpPost);
        return httpClient.execute(httpPost);
    }

    private SimpleWebScriptResult invokeWebScriptUsingPost(String wsURL, HashMap<String, String> parameters) throws WebScriptException {
        HttpClient httpClient = getHttpClient();
    	HttpResponse response;
    	String serviceUrl = getServiceUrl(wsURL);
    	
		try {			
			response = post(httpClient, serviceUrl, parameters);
			return parseWebScriptResponse(response); 
		} catch (Exception e) {
			String errorMessage = String.format("Error doing POST to %s", serviceUrl);
			log.error(errorMessage, e);
			throw new WebScriptException(errorMessage);
		}         
    }

    private SimpleWebScriptResult invokeWebScriptUsingGet(String wsURL) throws ClientProtocolException, URISyntaxException, IOException {
        HttpClient httpClient = getHttpClient();
    	HttpResponse response = get(httpClient, getServiceUrl(wsURL), new HashMap<String, String>());
        return parseWebScriptResponse(response);   
	}

    private String getServiceUrl(String wsURL) {
        return String.format("%s%s", serviceURL, wsURL);
    }

    private String getVersionDownloadUrl(Document document) throws Exception {
        String encodedDocumentName = URLEncoder.encode(document.getName(), "UTF-8");
        String encodedDocumentPath = getDocumentPathForCMISWebScript(document);
        
        if (document.isLastVersion()) {
            String contentServiceURL = getServiceUrl(String.format("/cmis/p/%s/%s/content.pdf", encodedDocumentPath, encodedDocumentName));
            return getUrl(contentServiceURL).toString();
        } 

        String cmisWsURL = String.format("/cmis/p/%s/%s/versions", encodedDocumentPath, encodedDocumentName);
        HttpClient httpClient = getHttpClient();
    	HttpResponse response = get(httpClient, getServiceUrl(cmisWsURL), new HashMap<String, String>());
        HttpEntity httpEntity = response.getEntity();
        String cmisXMLDocument = EntityUtils.toString(httpEntity);    
        String documentVersion = document.getDocumentVersion();
        
        try {
        	return extractDocumentURLFromCMISXML(cmisXMLDocument, documentVersion);
        } catch (Exception e) {
        	log.error(String.format("Document version not found in '%s'", cmisWsURL), e);
        	throw new Exception(String.format("Document version not found in '%s'", cmisWsURL));	
        }        
        
    }

     private String extractDocumentURLFromCMISXML(String cmisXMLDocument, String documentVersion) throws Exception {
         DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
         org.w3c.dom.Document doc = dBuilder.parse(new ByteArrayInputStream(cmisXMLDocument.getBytes("UTF-8")));

         XPathFactory xPathfactory = XPathFactory.newInstance();
         XPath xpath = xPathfactory.newXPath();
         XPathExpression expr = xpath.compile("/feed/entry/object/properties/propertyString[@propertyDefinitionId='cmis:versionLabel']/value");

         NodeList entries = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
         for (int entryIndex = 0; entryIndex < entries.getLength(); entryIndex++) {
         	Node versionValueNode = entries.item(entryIndex);
         	String versionValue = versionValueNode.getTextContent();
			if (versionValue.equals(documentVersion)) {
         		Element entryNode = (Element) versionValueNode.getParentNode( // propertyString
         							).getParentNode( // properties
         							).getParentNode( // object
         							).getParentNode(); // entry
         		Node documentElement = entryNode.getElementsByTagName("content").item(0);
				return documentElement.getAttributes().getNamedItem("src").getNodeValue();
         	}
         }
         throw new Exception("Document version not found");
	}

	public void getContent(Document document, OutputStream outputStream) throws Exception {      
		InputStream content;
		HttpEntity httpContentEntity;
		String downloadURL = getVersionDownloadUrl(document);
        log.info(String.format("Getting content from: %s", downloadURL));
        
        HttpGet httpGet = new HttpGet(downloadURL);
        HttpClient httpClient = getHttpClient();
        beforeGet(httpClient, httpGet);
        HttpResponse downloadResponse = httpClient.execute(httpGet);

		int statusCode = downloadResponse.getStatusLine().getStatusCode();
		if (statusCode >= 400) {
        	String message = String.format("Content not found (status code: %s, url: %s)", statusCode, downloadURL);
        	log.error(message);
			throw new Exception(message);
        }
        httpContentEntity = downloadResponse.getEntity();
        content = httpContentEntity.getContent();
		outputStream.write(IOUtils.toByteArray(content));

    }

	private String getDocumentPathForCMISWebScript(Document document)
			throws UnsupportedEncodingException {
		String documentPath = absolutePathForCustomer(document.getCustomer().getIdPersona());
        List<String> pathParts = new ArrayList<String>();
        String encodedDocumentPath;
        
        for (String documentPathPart : documentPath.split("/")) {
        	String part = documentPathPart.trim();
        	if (!part.isEmpty()) {
        		pathParts.add(URLEncoder.encode(part, "UTF-8"));
        	}
        }
        pathParts.remove(0); // Company Home folder is implicit
        encodedDocumentPath = StringUtils.join(pathParts, "/");
		return encodedDocumentPath;
	}

	public byte[] getContentAsByteArray(Document document) throws Exception {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		getContent(document, baos);
		return baos.toByteArray();
	}
	
	public SimpleWebScriptResult createFolders(String folderPath) throws ClientProtocolException, URISyntaxException, IOException, WebScriptException {        
        String wsURL = "/coop/bancocredicoop/proyectos/gd/createfolders.json";
        HashMap<String, String> parameters = new HashMap<String, String>();
        parameters.put("rootPath", alfrescoConfig.getRootPath());
        parameters.put("folder", folderPath);
        return invokeWebScriptUsingPost(wsURL, parameters);
    }

    public WebScriptResultWithVersion uploadFile(String documentName, Document document, InputStream documentInputStream) throws ClientProtocolException, URISyntaxException, IOException, WebScriptException {
    	
    	Date validTo = document.getValidTo();
    	Long idPersona = document.getCustomer().getIdPersona();
    	String documentVersion = document.getDocumentVersion();
		String title = document.getDocumentType().getShortName();
		
    	String wsURL = "/coop/bancocredicoop/proyectos/gd/upload.json";
    	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'00:00:00-03:00");
    	String validToString = dateFormat.format(validTo);
        String folderPath = absolutePathForCustomer(idPersona);
        
        SimpleWebScriptResult createFoldersResult = createFolders(folderPath);
        if (!createFoldersResult.isSuccessful()) {
        	log.error(String.format("Error creating folders: %s", createFoldersResult.getMessage()));
            return new WebScriptResultWithVersion(createFoldersResult);
        }
        
        HashMap<String, String> parameters = new HashMap<String, String>();
        parameters.put("folderPath", folderPath);
        parameters.put("title", title);
        parameters.put("documentName", documentName);
        parameters.put("validTo", validToString);
        parameters.put("documentVersion", documentVersion);
        parameters.put("tags", ""); // not used, to avoid error in webscript

        HttpClient httpClient = getHttpClient();
     	HttpResponse response;
     	String serviceUrl = getServiceUrl(wsURL);
     	
 		try {			
 			response = postMultipart(httpClient, serviceUrl, documentName, documentInputStream, parameters);
 			return parseWebScriptResponseWithData(response); 
 		} catch (Exception e) {
 			String errorMessage = String.format("Error doing POST to %s", serviceUrl);
 			log.error(errorMessage, e);
 			SimpleWebScriptResult simpleWebScriptResult = new SimpleWebScriptResult(false, errorMessage);
			return new WebScriptResultWithVersion(simpleWebScriptResult);
 		}         
        
    }

	public SimpleWebScriptResult hello() throws ClientProtocolException, URISyntaxException, IOException {
        String wsURL = "/coop/bancocredicoop/proyectos/gd/hello.json";
        return invokeWebScriptUsingGet(wsURL);
    }


	public SimpleWebScriptResult addUserToGroup(String userName, String groupName) throws WebScriptException {
        String wsURL = "/coop/bancocredicoop/proyectos/gd/addusertogroup.json";
        HashMap<String, String> parameters = new HashMap<String, String>();
        parameters.put("userName", userName);
        parameters.put("groupName", groupName);
        return invokeWebScriptUsingPost(wsURL, parameters);
    }

    public SimpleWebScriptResult removeUserFromGroup(String userName, String groupName) throws WebScriptException {
        String wsURL = "/coop/bancocredicoop/proyectos/gd/removeuserfromgroup.json";
        HashMap<String, String> parameters = new HashMap<String, String>();
        parameters.put("userName", userName);
        parameters.put("groupName", groupName);
        return invokeWebScriptUsingPost(wsURL, parameters);
    }

    private SimpleWebScriptResult parseWebScriptResponse(HttpResponse response) {

        int statusCode = response.getStatusLine().getStatusCode();
        Gson gson = new Gson();
        SimpleWebScriptResult result;
        String body;
        
        try {
        	body = EntityUtils.toString(response.getEntity());
        	result = gson.fromJson(body, SimpleWebScriptResult.class);
        } catch (Exception e) {
        	result = new SimpleWebScriptResult(false, String.format("HTTP Status: %s, message: n/a", statusCode));
        }
        
        if (statusCode >= 400 || !result.isSuccessful()) {
            String errorMessage = String.format("Error in Alfresco Web Script: %s", result.getMessage());
            return new SimpleWebScriptResult(false, errorMessage);
        } else {
            return result;
        }

    }
    
	private WebScriptResultWithVersion parseWebScriptResponseWithData(HttpResponse response) {
		
        int statusCode = response.getStatusLine().getStatusCode();
        Gson gson = new Gson();
        WebScriptResultWithVersion result;
        String body;
        
        try {
        	body = EntityUtils.toString(response.getEntity());
        	result = gson.fromJson(body, WebScriptResultWithVersion.class);
        } catch (Exception e) {
        	SimpleWebScriptResult simpleResult = new SimpleWebScriptResult(false, String.format("HTTP Status: %s, message: n/a", statusCode));
        	return new WebScriptResultWithVersion(simpleResult);
        }
        
        if (statusCode >= 400 || !result.isSuccessful()) {
            String errorMessage = String.format("Error in Alfresco Web Script: %s", result.getMessage());
            return new WebScriptResultWithVersion(new SimpleWebScriptResult(false, errorMessage));
        } else {
            return result;
        }
	}

    public String absolutePathForCustomer(Long idCustomer) {
        String normIdCustomer = StringUtils.leftPad(idCustomer.toString(), 10, "0");
        String lastTenChars = StringUtils.substring(normIdCustomer, normIdCustomer.length()-10, normIdCustomer.length());
        List<String> folders = new ArrayList<String>();
        
        folders.add(alfrescoConfig.getRootPath());
        for (int strIndex=0; strIndex < 5; strIndex++) {
        	folders.add(StringUtils.substring(lastTenChars, strIndex*2, strIndex*2+2));
        }

        folders.add(idCustomer.toString());
        return StringUtils.join(folders, "/");
    }

    HttpClient getHttpClient() {
        return HttpClients.createDefault();
    }
    
    public AlfrescoConfig getAlfrescoConfig() {
    	return alfrescoConfig;
    }

}